﻿using System;
using System.Collections.Generic;
using System.Linq;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel
{
    /// <summary>
    /// Implements an undoable command for creating a new dynamics progress mark.
    /// </summary>
    /// <remarks>
    /// Input Parameters:
    /// <list type="table">
    /// <listheader>
    /// <term>
    ///     Name
    /// </term>
    /// <description>
    ///     Description
    /// </description>
    /// </listheader>
    /// <item>
    /// <term>
    ///     <see cref="Start"/> (<see cref="Elements.CWNotation.DurationalSymbol"/>)
    /// </term>
    /// <description>
    ///     The durational symbol at which the mark will start.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="End"/> (<see cref="Elements.CWNotation.DurationalSymbol"/>)
    /// </term>
    /// <description>
    ///     The durational symbol at which the mark will end.
    ///     It must belong to the same part as <see cref="Start"/> and begin after the <see cref="Start"/>.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="ProgressType"/> (<see cref="Elements.CWNotation.DynamicsProgressType"/>)
    /// </term>
    /// <description>
    ///     The type of the dynamics progress represented by this mark.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Visualization"/> (<see cref="Elements.CWNotation.DynamicsProgressDisplayMode"/>)
    /// </term>
    /// <description>
    ///     The type of the mark's visualization.
    /// </description>
    /// </item>
    /// </list>
    /// </remarks>
    public class AddDynamicsProgressMarkCommand : UndoableCommandBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="AddDynamicsProgressMarkCommand"/> class.
        /// </summary>
        /// <param name="undoStack"></param>
        /// <param name="redoStack"></param>
        public AddDynamicsProgressMarkCommand(CommandStack undoStack, CommandStack redoStack)
            : base(undoStack, redoStack)
        {
            AddParameter<Elements.CWNotation.DurationalSymbol>("Start", ParameterDirection.In);
            AddParameter<Elements.CWNotation.DurationalSymbol>("End", ParameterDirection.In);
            AddParameter<Elements.CWNotation.DynamicsProgressType>("ProgressType", ParameterDirection.In);
            AddParameter<Elements.CWNotation.DynamicsProgressDisplayMode>("Visualization", ParameterDirection.In);

            createdMarks = new List<Elements.CWNotation.DynamicsProgressMark>();

            Name = "Create Dynamics Progress Mark";
            ProgressType = Elements.CWNotation.DynamicsProgressType.Crescendo;
            Visualization = Elements.CWNotation.DynamicsProgressDisplayMode.Text;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Processes the affected section.
        /// </summary>
        /// <param name="section">
        /// <para>
        /// Type: <see cref="Elements.MusicSection"/>
        /// </para>
        /// <para>
        /// The section.
        /// </para>
        /// </param>
        /// <param name="staffDescriptor">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor of the staff to which the mark was inserted.
        /// </para>
        /// </param>
        /// <param name="measure">
        /// <para>
        /// Type: <see cref="Model.Elements.MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure to which the mark was inserted.
        /// </para>
        /// </param>
        private void ProcessAffectedSection(Elements.MusicSection section, Model.Elements.CWNotation.StaffDescriptor staffDescriptor, Model.Elements.MeasureElement measure)
        {
            var modelSection = section.AdaptedElement as Model.Elements.MusicSectionElement;

            if (!modelSection.Parts.Contains(staffDescriptor.FindOwner<Model.Elements.PartDescriptorElement>()))
                return;

            var modelInterior = modelSection.Interior as Model.Elements.CWNotation.MusicSectionInterior;
            var modelSystem = modelInterior.Systems.First(s => s.Measures.Contains(measure));
            var modelStaff = modelSystem.Staves.FirstOrDefault(s => s.Descriptor == staffDescriptor);

            if (modelStaff == null)
                return;

            var interior = section.Interior as Elements.CWNotation.MusicSectionInterior;
            var system = interior.Systems.First(s => s.AdaptedElement == modelSystem);
            var staff = system.Staves.First(s => s.AdaptedElement == modelStaff);

            var modelMark = modelStaff.Attachments.First(a => a is Model.Elements.CWNotation.PresentedAttachment && ((Model.Elements.CWNotation.PresentedAttachment)a).PresentedElement == markElement)
                as Model.Elements.CWNotation.DynamicsProgressMark;
            modelMark.Visualization = (Model.Elements.CWNotation.DynamicsProgressDisplayMode)Enum.Parse(typeof(Model.Elements.CWNotation.DynamicsProgressDisplayMode), Visualization.ToString());
            createdMarks.Add(staff.Attachments.First(a => a.AdaptedElement == modelMark) as Elements.CWNotation.DynamicsProgressMark);

            if (modelSection.FindOwner<Model.Elements.PageElement>() != null)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(modelSystem);
            else
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(modelSystem);
        }

        #endregion

        #region Command Logic

        /// <inheritdoc/>
        protected override void CommandOperation(object parameter)
        {
            var staff = Start.FindOwner<Elements.CWNotation.Staff>();
            var modelStaff = (Model.Elements.CWNotation.Staff)staff.AdaptedElement;
            var modelStart = (Model.Elements.CWNotation.DurationalSymbol)Start.AdaptedElement;
            var measure = modelStart.Measure;
            var beat = modelStart.Beat;
            var endBeat = ((Model.Elements.CWNotation.DurationalSymbol)End.AdaptedElement).Beat;
            var composition = beat.FindOwner<Model.Elements.CompositionElement>();
            var part = modelStart.PresentedElement.FindOwner<Model.Elements.PartElement>().Descriptor;

            var length = endBeat.Beginning - beat.Beginning;
            for (ushort i = (ushort)beat.FindOwner<Model.Elements.MeasureElement>().Number; i < endBeat.FindOwner<Model.Elements.MeasureElement>().Number; ++i)
            {
                var content = composition[i][part].Content;
                if (content.Beats.Count > 0)
                {
                    var lastBeat = content.Beats[content.Beats.Count - 1];
                    length += lastBeat.Beginning;
                    if (lastBeat.Symbols.Count > 0)
                        length += lastBeat.Symbols.Max(s => s.Duration);
                }
            }

            markElement = new Model.Elements.DynamicsProgressElement(composition.PackagePart as Model.Packaging.CompositionPart, beat);
            markElement.Voices.Add(modelStart.PresentedElement.Voice);
            markElement.ProgressType = (Model.Elements.DynamicsProgressType)Enum.Parse(typeof(Model.Elements.DynamicsProgressType), ProgressType.ToString());
            markElement.Beats = (uint)length;
            modelStart.Measure[part].Content.AddAttachment(markElement, beat);

            var references = measure.FindOwner<Model.Elements.CompositionElement>().GetReferencingPages(measure, endBeat.FindOwner<Model.Elements.MeasureElement>());
            var project = Start.FindOwner<Elements.Project>();
            foreach (var reference in references)
            {
                var score = project.Scores.First(s => s.AdaptedPart == reference.Page.Owner);
                var page = score.Pages.First(p => p.AdaptedPart == reference.Page);
                var section = page.Sections.First(s => s.AdaptedElement == reference.Section) as Elements.MusicSection;

                ProcessAffectedSection(section, modelStaff.Descriptor, measure);
            }

            var vmComposition = project.Compositions.First(c => c.AdaptedElement == measure.FindOwner<Model.Elements.CompositionElement>());
            ProcessAffectedSection(vmComposition.LinearView.Content, modelStaff.Descriptor, measure);

            project.IsModified = true;
        }

        /// <inheritdoc/>
        protected override void RedoOperation()
        {
            markElement.FindOwner<Model.Elements.MusicalContentElement>().AddAttachment(markElement, markElement.FindOwner<Model.Elements.BeatElement>());

            foreach (var mark in createdMarks)
            {
                var staff = mark.FindOwner<Elements.CWNotation.Staff>();
                var modelStaff = staff.AdaptedElement as Model.Elements.CWNotation.Staff;
                modelStaff.Attachments[
                    modelStaff.Attachments.FindIndex(a => a is Model.Elements.CWNotation.DynamicsProgressMark && ((Model.Elements.CWNotation.DynamicsProgressMark)a).PresentedElement == markElement)
                    ] = mark.AdaptedElement as Model.Elements.CWNotation.DynamicsProgressMark;
                var vmMark = staff.Attachments.First(a => a.AdaptedElement == mark.AdaptedElement);
                staff.Attachments[staff.Attachments.IndexOf(vmMark)] = mark;

                if (modelStaff.FindOwner<Model.Elements.PageElement>() != null)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(modelStaff.FindOwner<Model.Elements.CWNotation.System>());
                else
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(modelStaff.FindOwner<Model.Elements.CWNotation.System>());
            }

            Start.FindOwner<Elements.Project>().IsModified = true;
        }

        /// <inheritdoc/>
        protected override void UndoOperation()
        {
            markElement.FindOwner<Model.Elements.MusicalContentElement>().RemoveAttachment(markElement);

            foreach (var mark in createdMarks)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(mark.AdaptedElement.FindOwner<Model.Elements.CWNotation.System>());

            Start.FindOwner<Elements.Project>().IsModified = true;
        }

        #endregion

        #region Public Properties
        
        /// <summary>
        /// Gets or sets the durational symbol at which the mark will end.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.CWNotation.DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The symbol at which the mark will end.<br/>
        /// This property wraps the "End" command parameter.
        /// </para>
        /// </value>
        public Elements.CWNotation.DurationalSymbol End
        {
            get { return GetParameterValue<Elements.CWNotation.DurationalSymbol>("End"); }
            set { SetParameterValue<Elements.CWNotation.DurationalSymbol>("End", value); }
        }

        /// <summary>
        /// Gets or sets the type of the dynamics progress.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.CWNotation.DynamicsProgressType"/>
        /// </para>
        /// <para>
        /// The type of the dynamics progress. Default value is <see cref="Elements.CWNotation.DynamicsProgressType.Crescendo"/><br/>
        /// This property wraps the "ProgressType" command parameter.
        /// </para>
        /// </value>
        public Elements.CWNotation.DynamicsProgressType ProgressType
        {
            get { return GetParameterValue<Elements.CWNotation.DynamicsProgressType>("ProgressType"); }
            set { SetParameterValue<Elements.CWNotation.DynamicsProgressType>("ProgressType", value); }
        }

        /// <summary>
        /// Gets or sets the durational symbol at which the mark will start.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.CWNotation.DurationalSymbol"/>
        /// </para>
        /// <para>
        /// The durational symbol at which the mark will start.<br/>
        /// This property wraps the "Start" command parameter.
        /// </para>
        /// </value>
        public Elements.CWNotation.DurationalSymbol Start
        {
            get { return GetParameterValue<Elements.CWNotation.DurationalSymbol>("Start"); }
            set { SetParameterValue<Elements.CWNotation.DurationalSymbol>("Start", value); }
        }

        /// <summary>
        /// Gets or sets the type of the visualization of the dynamics progress.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.CWNotation.DynamicsProgressDisplayMode"/>
        /// </para>
        /// <para>
        /// The type of the visualization of the dynamics progress. Default value is <see cref="Elements.CWNotation.DynamicsProgressDisplayMode.Text"/>.<br/>
        /// This property wraps the "Visualization" command parameter.
        /// </para>
        /// </value>
        public Elements.CWNotation.DynamicsProgressDisplayMode Visualization
        {
            get { return GetParameterValue<Elements.CWNotation.DynamicsProgressDisplayMode>("Visualization"); }
            set { SetParameterValue<Elements.CWNotation.DynamicsProgressDisplayMode>("Visualization", value); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the list of created marks.
        /// </summary>
        private List<Elements.CWNotation.DynamicsProgressMark> createdMarks;
        /// <summary>
        /// Holds the created dynamic mark element.
        /// </summary>
        private Model.Elements.DynamicsProgressElement markElement;

        #endregion
    }
}
